#!/usr/bin/env python
# -*- coding:utf8 -*-
"""
This library contains classes for IRC purpose.
"""
import bz2, codecs, imp, json, pickle, os, re, string, sys, urllib
from datetime import datetime
from util.parsers import GetParams
from ircfuncs import irc_lower
from util.functions import encode, escape_for_regexp, format_number

class FileStreamTypeError:
	"""Type Error: need another type. It was expected another type to append/update data."""

class Command:
	def __init__(self, msg, triggers, me):
		#print __name__, "Command", "m: %s t: %r" % (msg, triggers)
		if not msg: msg = ""
		trigger = re.match("([%s]|%s: )" % (triggers, me), msg, re.I)
		self.trigger = trigger.group(1) if trigger else ""
		self.cmdline = msg.replace(self.trigger, "", 1) if self.trigger else ""
		self.command = self.cmdline.split(" ", 1)[0]
		self.cmdline = self.cmdline.replace(self.command, "", 1).strip()
		self.command = self.command.lower()
		self.highlight = self.trigger == "%s: " % me

		self.opts = GetParams(self.cmdline)
		self.arguments = self.opts.arguments()
		self.parameters = self.opts.parameters()
		self.modifiers = self.opts.modifiers()
		self.action = self.opts.action()

	def keys(self):
		return {
			"trigger": self.trigger,
			"cmdline": self.cmdline,
			"command": self.command,
			"highlight": self.highlight,
			"opts": self.opts,
			"arguments": self.arguments,
			"parameters": self.parameters,
			"modifiers": self.modifiers,
		}

class Event:
	def __init__(self, src, source, target, command, parameters, msg, agent, ctcp, stalked):
		self.src = src
		self.source = source
		self.target = target
		self.type = command
		self.params = parameters
		self.msg = msg
		self.agent = agent
		self.ctcp = ctcp
		self.stalked = stalked
		self.identified = " " in src['hname'] if src and src['hname'] else True

	def keys(self):
		#for debugging use
		return {
			"src": self.src,
			"source": self.source,
			"target": self.target,
			"type": self.type,
			"params": self.params,
			"msg": self.msg,
			"agent": self.agent,
			"ctcp": self.ctcp,
			"stalked": self.stalked,
			"identified": self.identified
		}

class _Path:
	def __init__(self):
		self.home = self.preview(__file__.rsplit(os.sep, 1)[0])

	def append(self, path):
		"""append any folder to sys.append()"""
		sys.path.append(path)

	def join(self, *args):
		"""concatenate strings with path formatting"""
		return os.path.join(*args)

	def preview(self, path):
		"""preview(s) -> returns the higher level path.
		preview(s) -> torna la carpeta superior.
		"""
		return os.path.split(path)[0]

	def basename(self, filename):
		return os.path.basename(filename)

	def extension(self, filename):
		return os.path.splitext(self.basename(filename))[1]

	def name(self, filename):
		"""name(s) -> returns the filename without the extension."""
		return os.path.splitext(self.basename(filename))[0]

	def split(self, path):
		"""split(s) -> returns a 3-tuple (path, filename, extension)."""
		filename = self.basename(path)
		dirname = os.path.dirname(path)
		path = [self.home]
		path += dirname.replace(self.home+os.sep, "").split(os.sep)
		return path, self.name(filename), self.extension(filename)

	def files(self, path):
		if os.path.isdir(path):
			return os.listdir(path)
		else:
			print "ERROR: directory path expected"
			return []
_path = _Path()

class _Obj:
	def __init__(self, path, encoding):
		self.ext = _path.extension(path)
		self.path = path
		self.encoding = encoding
		self.stream = None

	def exists(self):
		return os.path.exists(self.path)

	#file modes
	def read(self):
		if self.ext == ".bin":
			self.stream = bz2.BZ2File(self.path, "r")
			return self.stream
		else:
			self.stream = codecs.open(self.path, "r", self.encoding)
			return self.stream

	def write(self):
		if self.ext == ".bin":
			self.stream = bz2.BZ2File(self.path, "w")
			return self.stream
		else:
			self.stream = codecs.open(self.path, "w", self.encoding)
			return self.stream

	def append(self):
		if self.ext == ".bin":
			self.stream = bz2.BZ2File(self.path, "w")
			return self.stream
		else:
			self.stream = codecs.open(self.path, "a", self.encoding)
			return self.stream

	#file methods
	def get(self):
		if self.exists():
			if self.stream and self.stream.mode == "rb":
				if self.ext == ".bin":
					data = pickle.load(self.read())
				elif self.ext == ".log":
					try:
						data = json.load(self.read(), self.encoding)
					except ValueError:
						data = self.stream.read()
				else:
					data = self.stream.read()
				return data
			else:
				self.read()
				return self.get()
		else:
			if self.ext in (".bin", ".log"):
				data = {}
			else:
				data = ""
			self.put(data)
			return data

	def put(self, data, clean=False):
		if clean:
			self.write()
		elif not clean and self.exists():
			if self.ext in (".bin", ".log"):
				old_data = self.get()
				if len(old_data) == 0 and isinstance(old_data, dict) and isinstance(data, list):
					old_data = list(old_data)
				elif len(old_data) > 0 and isinstance(old_data, dict) and not isinstance(data, dict):
					raise FileStreamTypeError("need dict, %s found" % type(data))
				elif len(old_data) > 0 and isinstance(old_data, list) and not isinstance(data, list):
					raise FileStreamTypeError("need list, %s found" % type(data))
				if isinstance(old_data, basestring):
					data = u"%s\r\n%s" % (old_data, data)
					self.append()
				elif isinstance(old_data, dict):
					print data
					old_data.update(data)
					data = old_data.copy()
				elif isinstance(old_data, list):
					print data
					old_data += data
					data = list(old_data)
			else:
				data = u"\r\n%s" % (data)
				self.append()
		elif not self.exists():
			self.write()
		if self.ext == ".bin":
			pickle.dump(data, self.write(),protocol=pickle.HIGHEST_PROTOCOL)
		elif self.ext == ".log":
			if isinstance(data, (dict, list, tuple)):
				json.dump(data, self.write(), encoding=self.encoding, indent=4)
			else:
				self.stream.write(data)
		else:
			self.stream.write(data)
		if self.ext != ".bin": self.stream.flush()
		else: self.stream.close()

	def open(self):
		os.startfile(self.path)

	def size(self):
		return os.path.getsize(self.path)

	def mtime(self):
		return datetime.fromtimestamp(os.path.getmtime(self.path)).strftime("%Y-%m-%d %H:%M:%S") #last modified

	def atime(self):
		return datetime.fromtimestamp(os.path.getatime(self.path)).strftime("%Y-%m-%d %H:%M:%S") #last access

	def ctime(self):
		return datetime.fromtimestamp(os.path.getctime(self.path)).strftime("%Y-%m-%d %H:%M:%S") #last change

class File:
	def __init__(self, name, path=None, encoding=None):
		"""
		It pretend create and keep objects that can read and write several
		types of files with the same methods.
		* types:
			"b": binary,
			"d": data,
			"t": text,
			"p": python script
		* modes:
			"a(ppend)",
			"w(rite)"
		"""
		self.name = _path.name(name)
		self.ext = _path.extension(name) or ".txt"
		self.path = path or _path.join(_path.home, "logs")
		self.basename = "%s%s" % (self.name, self.ext)
		self.full_path = "%s%s%s" % (self.path, os.sep, self.basename)
		self.encoding = encoding or "utf8"
		self.obj = _Obj(self.full_path, self.encoding)

	def load(self):
		if self.ext != ".py": return {}
		self.name = _path.name(self.name)
		stream, path, descr = imp.find_module(self.name, [self.path])
		return imp.load_module(self.name, stream, path, descr)

	def get(self):
		return self.obj.get()

	def put(self, data, clean=False):
		self.obj.put(data, clean)

	def close(self):
		self.obj.stream.close()

	def exists(self):
		return self.obj.exists()

	def size(self):
		return self.obj.size()

	def modified(self):
		return self.obj.mtime()

	def created(self):
		return self.obj.ctime()

	def accessed(self):
		return self.obj.atime()

	def filename(self):
		return self.basename

class Load:
	"""
	Load modules from a specified folder, and register its instances into a dictionary
	"""
	def __init__(self, folder):
		self.variables={}
		self.commands = {'skip': {}, 'high': {}, 'medium': {}, 'low': {}}
		self.folder=folder
		self.root = _path.join(_path.home, "commands")
		self._initialize()

	def _initialize(self):
		self.modules = []
		moduledir = _path.join(self.root, self.folder)
		for filename in sorted(os.listdir(moduledir)):
			if filename.endswith('.py') and not filename.startswith('_'):
				path, name, ext = _path.split(filename)
				try:
					module = File(filename, moduledir).load()
				except Exception, e:
					print >> sys.stderr, "Error loading %s: %s" % (name, e)
				else:
					if hasattr(module, 'load'):
						#if module has the variable 'load' as False, don't load this one. (See "hasattr(method, 'load')")
						if not module.load: continue
					field = module.field if hasattr(module, 'field') else name
					self._register(vars(module), name, field)
					self.modules.append(name)
		if self.modules:
			print >> sys.stderr, 'Registered modules:', ', '.join(self.modules)
		self._bindrules()

	def _register(self, variables, module, field):
		for name, obj in variables.iteritems():
			self.variables[name] = (obj, module, field)

	def _bindrules(self):
		all=[]; fields = []
		for name, (method, module, field) in self.variables.iteritems():
			if hasattr(method, 'rule'):
				#load: can be load this command?
				if hasattr(method, 'load') and not method.load:
					continue
				#name
				if not hasattr(method, 'name'):
					method.name = method.__name__
				#priority:get priority level
				if not hasattr(method, 'priority'):
					method.priority = 'medium'
				#evttypes: limit command to certain event types
				if not hasattr(method, "evttypes"):
					method.evttypes=("msg", "notice", "action")
				elif isinstance(method.evttypes, basestring):
					method.evttypes=(method.evttypes,)
				#thread: runcommand in a new thread
				if not hasattr(method, 'thread'):
					method.thread = False
				if not isinstance(method.rule, list):
					method.rule = [method.rule]
				#aliases: add other commands rules
				if not hasattr(method, 'aliases'):
					method.aliases = []
				elif isinstance(method.aliases, basestring):
					method.aliases = [method.aliases]
				elif isinstance(method.aliases, tuple):
					method.aliases = list(method.aliases)
				#fullmsg: indicates if cmd must be read as whole line
				if not hasattr(method, 'fullmsg'):
					method.fullmsg = False
				#limit: restrict times that cmd can be used into a minute
				if not hasattr(method, 'limit'):
					method.limit = 3
				#level: restrict access according to user level
				if not hasattr(method, 'level'):
					method.level="*"
				#lowlvl:what lower userlevel can run this command
				if not hasattr(method, 'lowlvl'):
					method.lowlvl = "user"
				#channels: nameof this command in certain channels
				if not hasattr(method, 'channels'):
					method.channels={}
				#chanlang: name of this command according to its channel language
				if not hasattr(method, 'chanlang'):
					method.chanlang={}
				else:
					for k, v in method.chanlang.copy().iteritems():
						if not isinstance(v, list):
							method.chanlang[k] = [v]
				#onchan: restrictcommand to certain channels
				if not hasattr(method, 'onchan'):
					method.onchan = []
				elif isinstance(method.onchan, basestring):
					method.onchan = [method.onchan]
				#help
				if isinstance(method.__doc__, basestring):
					method.help = method.__doc__
				else:
					method.help = "no such help."
				#fix config forgettings
				if "*" in method.evttypes:
					method.evttypes=("*",)
					method.rule=False
					method.skip_sentinel=True

				if isinstance(method.rule, (list,tuple)):
					method.aliases += method.rule[1:]
					method.rule= [method.rule[0]]
				method.field = field
				method.module = module

				self.commands[method.priority][method.name]=method
				all.append(method.name)

		print "\t", ", ".join(all)

class Dicts:
	"""
	It pretends to be a dictionary that it can save content and
	its data can be used after succcessive runnings.
	"""
	def __init__(self, log, dir=None):
		self._logname = "%s.bin" % log
		self._dir = dir
		self._log = File(self._logname, self._dir).get()

	def __call__(self):
		return self._log

	def __delitem__(self, key):
		del self._log[key]

	def __getitem__(self, key):
		return self._log.get(key)

	def __setitem__(self, key, value):
		self._log[key] = value

	def __len__(self):
		return len(self._log)

	def length(self):
		return len(self._log)

	def get(self, key, failvalue=None):
		return self._log.get(key, failvalue)

	def update(self, key, value):
		self._log[key] = value
		self.save()

	def delete(self, value):
		"""delete(key) --> value. Remove specified key, and return value."""
		rtn = self._log.pop(value)
		self.save()
		return rtn

	def clear(self):
		self._log.clear()
		self.save()

	def keys(self):
		return self._log.keys()

	def items(self):
		return self._log.items()

	def iteritems(self):
		return self._log.iteritems()

	def values(self):
		return self._log.values()

	def has_key(self, key):
		return self._log.has_key(key)

	def copy(self):
		return self._log.copy()

	def name(self):
		return self._logname

	def read(self):
		return self._log

	def save(self, skip_old_data = True):
		"""if skip_old_data == True: the data will not be appended but overwriten"""
		File(self._logname, self._dir).put(self._log, skip_old_data)

class IRCDict:
	"""A dictionary suitable for storing IRC-related things.

	Dictionary keys a and b are considered equal if and only if
	irc_lower(a) == irc_lower(b)

	Otherwise, it should behave exactly as a normal dictionary.
	"""

	def __init__(self, dict=None):
		self.data = {}
		self.canon_keys = {}  # Canonical keys
		if dict is not None:
			self.update(dict)

	def __repr__(self):
		return repr(self.data)

	def __cmp__(self, dict):
		if isinstance(dict, IRCDict):
			return cmp(self.data, dict.data)
		else:
			return cmp(self.data, dict)

	def __len__(self):
		return len(self.data)

	def __getitem__(self, key):
		return self.data[self.canon_keys[irc_lower(key)]]

	def __setitem__(self, key, item):
		if key in self:
			del self[key]
		self.data[key] = item
		self.canon_keys[irc_lower(key)] = key

	def __delitem__(self, key):
		ck = irc_lower(key)
		del self.data[self.canon_keys[ck]]
		del self.canon_keys[ck]

	def __iter__(self):
		return iter(self.data)

	def __contains__(self, key):
		return self.has_key(key)

	def clear(self):
		self.data.clear()
		self.canon_keys.clear()

	def copy(self):
		if self.__class__ is UserDict:
			return UserDict(self.data)
		import copy
		return copy.copy(self)

	def keys(self):
		return self.data.keys()

	def items(self):
		return self.data.items()

	def values(self):
		return self.data.values()

	def has_key(self, key):
		return irc_lower(key) in self.canon_keys

	def update(self, dict):
		for k, v in dict.items():
			self.data[k] = v

	def get(self, key, failobj=None):
		return self.data.get(key, failobj)

	def pop(self, key, failobj=""):
		return self.data.pop(key, failobj)

class Channel:
	#from irclib.py (but with some fixes, and changes)
	"""A class for keeping information about an IRC channel.

	This class can be improved a lot.
	"""

	def __init__(self):
		# changed: only one dict is necessary, I changed:
		#   self.userdict[nick] = 1 to self.userdict[nick] = "u" for a user
		#   self.userdict[nick] = 1 to self.userdict[nick] = "o" for an oper
		#   self.userdict[nick] = 1 to self.userdict[nick] = "v" for a voiced

		self.userdict = IRCDict()
		self.modes = {}

	def users(self):
		"""Returns an unsorted list of the channel's users."""
		return self.userdict.keys()

	def opers(self):
		"""Returns an unsorted list of the channel's operators."""
		opers = []
		for user in self.userdict.keys():
			if self.userdict[user] == "o":
				opers.append(user)
		return opers

	def voiced(self):
		"""Returns an unsorted list of the persons that have voice
		mode set in the channel."""
		voiced = []
		for user in self.userdict.keys():
			if self.userdict[user] == "v":
				voiced.append(user)
		return voiced

	def has_user(self, nick):
		"""Check whether the channel has a user."""
		return nick in self.userdict

	def is_oper(self, nick):
		"""Check whether a user has operator status in the channel."""
		return self.userdict.get(nick, "") == "o"

	def is_voiced(self, nick):
		"""Check whether a user has voice mode set in the channel."""
		return self.userdict.get(nick, "") == "v"

	def add_user(self, nick):
		self.userdict[nick] = "u"

	def remove_user(self, nick):
		if nick in self.userdict:
			del self.userdict[nick]

	def change_nick(self, before, after):
		self.userdict[after] = self.userdict[before]
		# fixed: if user only swap case, dont delete her/him
		# else user will disappear from dict.
		# --Coet 2010.03.11 03:03:03 CEST
		if irc_lower(after) != irc_lower(before):
			del self.userdict[before]

	def set_mode(self, mode, value=None):
		"""Set mode on the channel.

		Arguments:

			mode -- The mode (a single-character string).

			value -- Value
		"""
		if mode in ("o", "v"):
			self.userdict[value] = mode
		else:
			self.modes[mode] = value

	def clear_mode(self, mode, value=None):
		"""Clear mode on the channel.

		Arguments:

			mode -- The mode (a single-character string).

			value -- Value
		"""
		try:
			if mode in ("o", "v"):
				del self.userdict[value]
			else:
				del self.modes[mode]
		except KeyError:
			pass

	def has_mode(self, mode):
		return mode in self.modes

	def is_moderated(self):
		return self.has_mode("m")

	def is_secret(self):
		return self.has_mode("s")

	def is_protected(self):
		return self.has_mode("p")

	def has_topic_lock(self):
		return self.has_mode("t")

	def is_invite_only(self):
		return self.has_mode("i")

	def has_allow_external_messages(self):
		return self.has_mode("n")

	def has_limit(self):
		return self.has_mode("l")

	def limit(self):
		if self.has_limit():
			return self.modes[l]
		else:
			return None

	def has_key(self):
		return self.has_mode("k")

	def key(self):
		if self.has_key():
			return self.modes["k"]
		else:
			return None

class Logs:
	def __init__(self):
		self.users = Dicts("users")
		self.IPs = Dicts("ips")
		self.defs = Dicts("definitions")
		self.logs = Dicts("log")
		self.igns = Dicts("igns")
		self.otherbots = File("bots.log")
		try:
			self.interested = self.logs['interested']
			self.usersQueue = self.logs['usersQueue']
		except:
			self.logs.update('interested', ["coet", "lluis_tgn", "Mafoso-Espieta", "krls-ca", "Vriullop", "Lohen", "Paucabot", "SMP_ca", u'xtv-ca'] )  #29-2-08 retire u'Iradigalesc'
			self.logs.update("authorized", ["coet", "krls-ca", "Lohen", "SMP", "Vriullop"])
			self.logs.update('usersQueue', [])
			self.interested = self.logs['interested']
			self.usersQueue = self.logs['usersQueue']

def fmt(string, *args, **keywords):
	"""
	Format string with given arguments or keywords.
	fmt("$2 $1; $n $3|t is $2|s €", "of", "the price", n="this", t="carr", s=7450.678) -> the price of
	TODO:
		printf ("$1[aeiou]?l':el |n", n="claustre") -> "el claustre"
		printf ("The $-1[^s]?'s:'|n clock", n="father") -> "The father's clock"
		printf ("I got $n car$>1?s:|n;.", n=7) -> "I got 7 cars."
	"""
	enc="utf8"
	string=encode(string, enc)
	if not args and not keywords: return unicode(string, enc)
	tokens = re.findall(r"(\$\d+.?)", str(string))
	for token in tokens:
		if re.search("[^\d\w; ]", token[-1]): continue
		e = re.search("([$ ])", token[-1]).group(1) if re.search("([$ ])", token[-1]) else ""
		t = re.search("(\d+)", token).group(1)
		num = int(t)-1
		if num < len(args) and num >= 0:
			string = string.replace(token, str(args[num]) + e)

	tokens = re.findall("(\$([:\-\ddt.,' ]*\|)?(\w+))([ ;]?)", str(string))
	for token in tokens:
		key=token[2]
		if key.isdigit() and args:
			key=int(key)-1
			string = string.replace(token[0], args[key])
		if key in keywords:
			replacement = str(encode(keywords[key], enc))
			if isinstance(keywords, unicode): keywords[key]= encode(keywords[key], enc)
			if token[1]:
				if isinstance(keywords[key], basestring): #is text
					if ":" in token[1]:
						start, end = token[1][:-1].split(":")
						if not start: start=0
						if not end: end =len(replacement)
						start = int(start)
						end = int(end)
						replacement = replacement[start:end]
					else:
						pos = int(token[1][:-1])
						if pos == abs(pos): pos -=1
						replacement = replacement[pos]
				elif isinstance(keywords[key], (int, float)):
					thsep="."
					decsep=","
					places=2
					for i in re.findall("d[,\.']|t[,\.' ]", token[1]):
						if i[0]=="d":
							decsep = i[1]
						elif i[0] == "t":
							thsep = i[1]
					if decsep == thsep:
						import random
						thsep = list(",.' ")
						thsep.remove(decsep)
						thsep = random.choice(thsep)
					digits = re.search("(\d)", token[1])
					if digits:
						places=int(digits.group(1))
					replacement = format_number(float(keywords[key]), places, decsep, thsep)
			else:
				if isinstance(keywords[key], (int,float)):
					replacement = format_number(float(keywords[key]), 2)
			replacement += token[3] if token[3] == " " else ""
			string = string.replace(token[0]+token[3], replacement)
	return string

def printf(string, *args, **kwargs):
	print fmt(string, *args, **kwargs)
